/**
  altPreso preso.js
  Copyright (c) 2007 Teo Hui Ming <hmt@teohuiming.name>
  altPreso is freely distributable under the terms of an MIT-style license.
  For details, see the project web site, <http://code.google.com/p/altpreso>.
**/

/**
  Object: Preso
  The core slideshow presentation engine.
**/
var Preso = {

  /**
    Property: slides
    An array to store all slide elements.
  **/
  slides     : null,

  /**
    Property: curSlideId
    The current slide id.
  **/  
  curSlideId : null,
  
  _state     : 0,
  _states    : {'nil':0, 'normal':1, 'show':2},

  /**
    Function: isState
    Test Preso's current state.
    
    Arguments:
      state - 'nil' | 'normal' | 'show'
    
    Returns:
      true if the argument matches Preso's state; false otherwise.
  **/
  isState: function (state) {
    return (this._state == this._states[state]) ? true : false;
  },
  
  /**
    Function: setState
    Change Preso's current state.
    
    Arguements:
      newstate - 'nil' | 'normal' | 'show'
    
    Returns:
      true if Preso's state is set successfully; false otherwise.
  **/
  setState: function(newstate) {
    if ( this._states[newstate] ) {
      this._state = this._states[newstate];
      return true;
    }
    return false;
  },

  /**
    Function: init
    Get ready all slides and min/max box.
    
    Precondition:
      Preso.isState('nil') is true.
      
    Postcondition:
      Preso.isState('normal') is true.
  **/
  init: function () {
    if ( this.isState('nil') )
      ;
    else {return;}
    
    this.curSlideId = null;
    
    this.slides = document.getElementsByClassName(Preso.Class.SLIDE);
    if (this.slides.length == 0) {
      alert(Preso.Locale.ERR_NO_SLIDE_IN_PAGE);
      return;
    }
    
    for(var i = 0; i < this.slides.length; i++) {
      var id = Preso.Config.SLIDE_ID_PREFIX + (i+1); // 1-based
      this.slides[i].id = id;
      this.slides[i].index = i;
      this.slides[id] = Preso.slides[i];  // for easy access
    }
    
    Preso.Incremental.init(Preso.slides);
    Preso.CtrlBox.init();
    Preso.CtrlBox.toggleBox(Preso.CtrlBox.MIN_BOX);
    this.setState('normal');
  },
  
  /**
    Function: turnOn
    Turn on the slideshow.
    
    Precondition:
      Preso.isState('normal') is true.
    
    Postcondition:
      Preso.isState('show') is true.
      'turnedOn' event is broadcasted.
  **/
  turnOn: function () {
    if ( this.isState('normal') )
      ;
    else {return;}

    for (var i = 0; i < this.slides.length; i++) {
      var slide = this.slides[i];
      Element.removeClassName(slide, Preso.Class.CUR_SLIDE);
      Element.removeClassName(slide, Preso.Class.SLIDE);
      Element.addClassName(slide, Preso.Class.HIDE_SLIDE);
    }
    
    Element.addClassName(document.getElementsByTagName('body')[0],
                         Preso.Class.HIDE_BODY);
    
    Preso.CtrlBox.toggleBox(Preso.CtrlBox.MAX_BOX);
    this.setState('show');
    this.broadcastEvent('turnedOn');
        
    // display initial slide
    if (0 != window.location.hash.substring(1).length) {
      // exist URL fragment identifier (e.g. '#slide2')
      this.gotoSlide(window.location.hash.substring(1));
    }
    else {
      this.gotoSlide(this.slides[0].id);
    }
  },
  
  /**
    Function: turnOff
    Turn off the slideshow.
    
    Precondition:
      Preso.isState('show') is true.
    
    Postcondition:
      Preso.isState('normal') is true.
      'turnedOff' event is broadcasted.
  **/
  turnOff: function () {
    if ( this.isState('show') )
      ;
    else {return;}
    
    Preso.Incremental.turnOff();

    for (var i = 0; i < this.slides.length; i++) {
      var slide = this.slides[i];
      Element.addClassName(slide, Preso.Class.SLIDE);
      Element.removeClassName(slide, Preso.Class.CUR_SLIDE);
      Element.removeClassName(slide, Preso.Class.HIDE_SLIDE);
    }

    Element.removeClassName(document.getElementsByTagName('body')[0],
                            Preso.Class.HIDE_BODY);
    
    this.curSlideId = null;
    Preso.CtrlBox.toggleBox(Preso.CtrlBox.MIN_BOX);
    this.setState('normal');
    
    this.broadcastEvent('turnedOff');
  },

  /**
    Function: setCurrentSlide
    Set a slide as current slide. *Internal Use*.
    
    Arguments:
      desiredSlideId - string value of the slide id.
    
    Precondition:
      Preso.isState('show') is true.
      
    Postcondition:
      'slideChanged' event is broadcasted.
    
    Remarks:
      Never call this function directly, ALWYAS use <Preso.gotoSlide>.
  **/
  setCurrentSlide: function (desiredSlideId) {
    if ( this.isState('show') )
      ;
    else {return;}

    var pattern = '^' + Preso.Config.SLIDE_ID_PREFIX + '[0-9]+$';
    var regexp = new RegExp(pattern);
    
    if ( !regexp.test(desiredSlideId) ||  //syntax invalid or,
         !this.slides[desiredSlideId]     //no such slideid
       ) {
      alert(Preso.Locale.ERR_SLIDE_ID_NOT_FOUND + desiredSlideId);
      // redisplay current slide, otherwise the first slide
      desiredSlideId = (null == this.curSlideId)
              ? this.slides[0].id
              : this.curSlideId;
    }
    
    if (null != this.curSlideId) {
       var theLegacyOne = this.slides[this.curSlideId];
       Element.removeClassName(theLegacyOne, Preso.Class.CUR_SLIDE);
       Element.addClassName(theLegacyOne, Preso.Class.HIDE_SLIDE);
    }
    
    var theChosenOne = this.slides[desiredSlideId];
    Element.removeClassName(theChosenOne, Preso.Class.HIDE_SLIDE);
    Element.addClassName(theChosenOne, Preso.Class.CUR_SLIDE);

    this.curSlideId = desiredSlideId;
    
    // to allow keyboard scrolling on current slide, buggy.
    if (theChosenOne.focus) {theChosenOne.focus();}
    if (theChosenOne.blur)  {theChosenOne.blur();}

    this.broadcastEvent('slideChanged');
  }
};

////////////////////////////////////////////////////////////////////////

/**
  Object: Preso.CtrlBox
  Handle minbox and maxbox. *Internal Use*.
**/
Preso.CtrlBox = {

  /**
    Property: MIN_BOX
    A constant value represents minbox. *Public Use*
  **/
  MIN_BOX : 'minbox',
  
  /**
    Property: MAX_BOX
    A constant value represents maxbox. *Public Use*
  **/
  MAX_BOX : 'maxbox',
  
  _box    : null,
  
  /**
    Function: init
    Prepare minbox and maxbox by calling <Preso.CtrlBox.drawCtrlBoxHandler>.
   
    See also:
      <Preso.CtrlBox.drawCtrlBoxHandler>
  **/
  init: function() {   
    this._box = new Array();
    this._box[this.MIN_BOX] = this.drawCtrlBoxHandler(Preso.slides, this.MIN_BOX);
    this._box[this.MAX_BOX] = this.drawCtrlBoxHandler(Preso.slides, this.MAX_BOX);
  },
  
  /**
    Function: toggleBox
    Toggle to display either minbox or maxbox.
    
    Arguments:
      minOrMax - Preso.CtrlBox.MIN_BOX | Preso.CtrlBox.MAX_BOX
  **/
  toggleBox: function (minOrMax) {
    var body = document.getElementsByTagName('body')[0];
    
    //remove undesired box
    switch (minOrMax) {
      case this.MIN_BOX:
        var theBoxId = this.MIN_BOX;
        var maxbox;
        if(maxbox = document.getElementById(this.MAX_BOX)) {
          body.removeChild(maxbox);
        }
        break;
      case this.MAX_BOX:
        var theBoxId = this.MAX_BOX;
        var minbox;
        if(minbox = document.getElementById(this.MIN_BOX)) {
          body.removeChild(minbox);
        }
        break;
      default: return;
    }
    // create and display the box
    var ctrlbox = document.getElementById(theBoxId);
    if (!ctrlbox) {
      ctrlbox = document.createElement('div');
      ctrlbox.setAttribute('id', theBoxId);
      body.appendChild(ctrlbox);
    }
    ctrlbox.innerHTML = this._box[theBoxId];
  }
};

////////////////////////////////////////////////////////////////////////

/**
  Object: Preso.Incremental
  Handle the incremental display. *Internal Use*.
**/
Preso.Incremental = {
  curIncrementSlideId : null,    //current slideId of current incrementIndex
  curIncrementIndex   : null,    //current incrementIndex
  incrementals        : null,    //all incrementals of each slide in array

  /**
    Function: init
    Collect all incremental elements to get ready for incremental display.
    
    Arguments:
      slides - an array containing all slide elements.
      
    Remarks:
      Intended to be called by <Preso.init>.
  **/
  init: function (slides) {
    this.curIncrementSlideId = null;
    this.curIncrementIndex = null;
    this.incrementals = new Array();
    
    for (var i = 0; i < slides.length; i++) {
       var incArray = this.getIncrementals(slides[i]);
       if (incArray.length > 0) {
         this.incrementals[slides[i].id] = incArray;
       }
    }
  },

  /**
    Function: getIncrementals
    Return an array of all incremental elements within a slide.
    
    Arguments:
      element - a slide element object.
    
    Returns:
      An array of all incremental elements in the slide.
  **/
  getIncrementals: function (element) {
    /* prepares and returns an array of incrementals of a slide */
    var arrayToReturn = new Array();
    
    if (!element) {return arrayToReturn;} //recursive's fix point
    
    var children = element.childNodes;
    for (var i = 0; i < children.length; i++) {
      var child = children[i];
      
      //prepare and collect 'incremental' tagged elements
      if (1 == child.nodeType && Element.hasClassName(child, Preso.Class.INC)) {
        if ('ol' == child.nodeName.toLowerCase() || 
            'ul' == child.nodeName.toLowerCase() ||
            'dl' == child.nodeName.toLowerCase()) {
          
          Element.removeClassName(child, Preso.Class.INC);
          for (var j = 0; j < child.childNodes.length; j++) {
            if (1 == child.childNodes[j].nodeType) {  //is ELEMENT_NODE
              Element.addClassName(child.childNodes[j], Preso.Class.INC);
            }
          }//for
        }
        else {
          arrayToReturn[arrayToReturn.length] = child;
          Element.removeClassName(child, Preso.Class.INC);
        }
      }
      //process 'show-first' tagged list-elements. show-first:'show first child'
      if ( 1 == child.nodeType && 
           Element.hasClassName(child, Preso.Class.SHOW_FIRST) ) {
          
        Element.removeClassName(child, Preso.Class.SHOW_FIRST);
        if ('ol' == child.nodeName.toLowerCase() || 
            'ul' == child.nodeName.toLowerCase() ||
            'dl' == child.nodeName.toLowerCase()) {
          
          if ( child.hasChildNodes() && 
               (1 == first_child(child).nodeType) ) {
            Element.removeClassName(first_child(child), Preso.Class.INC);
          }
        }//if ol/ul
      }
      arrayToReturn = arrayToReturn.concat(this.getIncrementals(child));
    }
    return arrayToReturn;
  },
    
  /**
    Function: turnOff
    Remove incremental-display related class attribute values.
    
    Remarks:
      Intended to be called by <Preso.turnOff>
  **/
  turnOff: function () {
    
    this.curIncrementSlideId = null;
    this.curIncrementIndex = null;
    
    for (var i = 0; i < this.incrementals.length; i++) {
      var inc = this.incrementals[i];
      for (var j = 0; j < inc.length; j++) {
        Element.removeClassName(inc[j], Preso.Class.CUR_INC);
        Element.removeClassName(inc[j], Preso.Class.INC);
      }
    }
  },

  /**
    Function: prepareForIncrement
    Prepare a slide for first increment display.
    
    Arguments:
      slideid  - string value of the slide id.
  **/
  prepareForIncrement: function (slideid) {
    if ( !this.incrementals[slideid] ) {
      this.curIncrementSlideId = null;
      this.curIncrementIndex   = null;
      return;
    }

    var theOne = this.incrementals[slideid];
    this.curIncrementSlideId = slideid;
    this.curIncrementIndex = -1;

    for (var i = 0; i < theOne.length; i++) {
      Element.removeClassName(theOne[i], Preso.Class.CUR_INC);
      Element.addClassName(theOne[i], Preso.Class.INC);
    }
  },
  
  /**
    Function: prepareForDecrement
    Prepare a slide for first decrement display;
    
    Arguments:
      slideid  - string value of the slide id.
  **/
  prepareForDecrement: function (slideid) {
    if ( !this.incrementals[slideid] ) {
      this.curIncrementSlideId = null;
      this.curIncrementIndex   = null;
      return;
    }
    
    var theOne = this.incrementals[slideid];
    this.curIncrementSlideId = slideid;
    this.curIncrementIndex = theOne.length - 1; //point to last index
    
    for (var i = 0; i < theOne.length; i++) {
      Element.removeClassName(theOne[i], Preso.Class.CUR_INC);
      Element.removeClassName(theOne[i], Preso.Class.INC);
    }
    Element.addClassName(theOne[this.curIncrementIndex], Preso.Class.CUR_INC);
  },
  
  /**
    Function: increment
    Perform an increment step.
    
    Precondition:
      Preso.Incremental.canIncrement() is true.
  **/
  increment: function () {
    var theOne = this.incrementals[this.curIncrementSlideId];
    if (this.curIncrementIndex > -1) {
      Element.removeClassName(theOne[this.curIncrementIndex], Preso.Class.CUR_INC);
    }
    this.curIncrementIndex++; 
    if (this.curIncrementIndex < theOne.length) {
      Element.removeClassName(theOne[this.curIncrementIndex], Preso.Class.INC);
      Element.addClassName(theOne[this.curIncrementIndex], Preso.Class.CUR_INC);
    }
  },
  
  /**
    Function: decrement
    Perform a decrement step.
    
    Precondition:
      Preso.Incremental.canDecrement() is true.
  **/
  decrement: function () {
    var theOne = this.incrementals[this.curIncrementSlideId];
    if (this.curIncrementIndex < theOne.length) {
      Element.removeClassName(theOne[this.curIncrementIndex], Preso.Class.CUR_INC);
      Element.addClassName(theOne[this.curIncrementIndex], Preso.Class.INC);
    }
    this.curIncrementIndex--;
    if (this.curIncrementIndex > -1) {
      Element.addClassName(theOne[this.curIncrementIndex], Preso.Class.CUR_INC);
    }
  },
  
  /**
    Function: canIncrement
    To test whether can perform an increment step on current slide.
      
    Returns:
      true if an increment is still allowed, otherwise false.
  **/
  canIncrement: function () {
    return (
    this.curIncrementSlideId &&
    this.curIncrementIndex < this.incrementals[this.curIncrementSlideId].length - 1)
    ? true
    : false;
  },

  /**
    Function: canDecrement
    To test whether can perform a decrement step on current slide.
    
    Returns:
      true if a decrement is still allowed, otherwise false.
  **/
  canDecrement: function () {
    return (
        this.curIncrementSlideId &&
        this.curIncrementIndex > -1)
        ? true
        : false;
  }
};

////////////////////////////////////////////////////////////////////////

/**
  Function Preso.gotoSlide
  Go to a specific slide.
  
  Arguments:
    slideid  - string value of the slide id.
**/
Preso.gotoSlide = function (slideid) {  
  if (Preso.Config.enableIncremental) {
    Preso.Incremental.prepareForIncrement(slideid);
  }
  Preso.setCurrentSlide(slideid);
}

/**
  Function: Preso.next
  Go to next increment step or else to next slide.
  
  Arguments:
    forceSkipIncremental  - true | false (default)
     
  Remarks:
    When forceSkipIncremental is false, one increment step.
    If no more increment step, go to next slide.
    If that's the last slide, stop.
**/
Preso.next = function (forceSkipIncremental) {
  if ( !forceSkipIncremental && 
       Preso.Config.enableIncremental && 
       Preso.Incremental.canIncrement() ) {
  
    Preso.Incremental.increment();
  }
  else {
    var newIndex = parseInt(Preso.slides[Preso.curSlideId].index) + 1;
    if (Preso.slides[newIndex]) {
      Preso.gotoSlide(Preso.slides[newIndex].id);
    }
  }
}

/**
  Function: Preso.prev
  Go to next decrement step or else to previous slide.
  
  Arguments:
    forceSkipIncremental  - true | false (default)
     
  Remarks:
    When forceSkipIncremental is false, one decrement step.
    If no more decrement step, go to previous slide.
    If that's the first slide, stop.
**/

Preso.prev = function (forceSkipIncremental) {
  if ( !forceSkipIncremental && 
       Preso.Config.enableIncremental && 
       Preso.Incremental.canDecrement() ) {

    Preso.Incremental.decrement();
  }
  else {
    var newIndex = parseInt(Preso.slides[Preso.curSlideId].index) - 1;
    if (Preso.slides[newIndex]) {        
      if (Preso.Config.enableIncremental) { //special case: prepare for decrement
        Preso.Incremental.prepareForDecrement(Preso.slides[newIndex].id);
      }
      Preso.setCurrentSlide(Preso.slides[newIndex].id);
    }
  }//else
}

/**
  Function: Preso.home
  Go to the first slide.
**/
Preso.home = function () {
  Preso.gotoSlide(Preso.slides[0].id);
}

/**
  Function: Preso.end
  Go to the last slide.
**/
Preso.end = function () {
  Preso.gotoSlide(Preso.slides[Preso.slides.length - 1].id);
}

////////////////////////////////////////////////////////////////////////

/**
  Available Events:
  [Event Name]   [Description]           [Callback arguments left-to-right]
  'slideChanged' current slide changed.  newCurrentSlideId, allSlidesInArray
  'turnedOn'     switch to show-mode.    <none>
  'turnedOff'    switch to normal-mode.  <none>
**/

Preso._listeners = new Array();

/**
  Function: Preso.addEventListener
  Register a callback function to be triggered when an event occurs.
  
  Arguments:
    eventName        - string value of the event name.
    listenerFunction - the callback function
**/
Preso.addEventListener = function (eventName, listenerFunction) {
  if (eventName && Preso._listeners[eventName])
    ;
  else {
    Preso._listeners[eventName] = new Array();
  }
  var arr = Preso._listeners[eventName];
  arr[arr.length] = listenerFunction;
}

/**
  Function: Preso.broadcastEvent
  Trigger callback functions associated to an event. *Internal Use*
  
  Arguments:
    eventName - string value of the event name.
**/
Preso.broadcastEvent = function (eventName) {
  if (eventName && 
      Preso._listeners[eventName] && Preso._listeners[eventName].length) {
    
    var callbacks = Preso._listeners[eventName];
    for (var i = 0; i < callbacks.length; i++) {
      switch (eventName) {
        case 'slideChanged':
            callbacks[i](Preso.curSlideId, Preso.slides);
          break;
        default:
            callbacks[i]();
      }
    }
  }
}

////////////////////////////////////////////////////////////////////////

/**
  Function: Preso.CtrlBox.drawCtrlBoxHandler
  A function to return HTML codes for either minbox or maxbox. *Internal Use*
  
  Arguments:
    slides   - an array of all slide elements.
    minOrMax - Preso.CtrlBox.MIN_BOX | Preso.CtrlBox.MAX_BOX
    
  Remarks:
    *Overwrite this function.*
    
    If minOrMax == Preso.CtrlBox.MIN_BOX, return HTML code for minbox.
    Otherwise, return HTML code for maxbox.
**/
Preso.CtrlBox.drawCtrlBoxHandler = function (slides, minOrMax) {return '';}

////////////////////////////////////////////////////////////////////////

/**
  Object: Preso.Class
  Contains class attribute values.
  
**/
Preso.Class = { 
// used in the slide authoring - for slide authors.
  SLIDE      : 'slide',
  INC        : 'incremental',
  SHOW_FIRST : 'show-first',

// used only during the slideshow - for developers.
  HIDE_BODY  : 'showmode-body',
  HIDE_SLIDE : 'showmode-slide',
  CUR_SLIDE  : 'current-slide',
  CUR_INC    : 'current'
};

/**
  Object: Preso.Config
  Contains configuration settings.
**/
Preso.Config = {
  SLIDE_ID_PREFIX   : 'slide', 
  enableIncremental : true
}; 

/**
  Object: Preso.Locale
  Contains phrases that require locale translation.
**/
Preso.Locale = {
  ERR_SLIDE_ID_NOT_FOUND  : 'No such slide id: ',
  ERR_NO_SLIDE_IN_PAGE    : 'No slide (element tagged with ' +
                            'class="slide"). Preso quit.'
};
